/*
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is ST_EXT-S_UIMAST.
 *
 * The Initial Developer of the Original Code is
 * University of Rome, Tor Vergata
 * Portions created by the Initial Developer are Copyright (C) 2009
 * the Initial Developer. All Rights Reserved.
 */

package it.uniroma2.art.uimast.impl;

import it.uniroma2.art.ontapi.*;
import it.uniroma2.art.ontapi.vocabulary.OWL;
import it.uniroma2.art.ontapi.vocabulary.RDFS;
import it.uniroma2.art.semanticturkey.resources.Resources;
import it.uniroma2.art.semanticturkey.vocabulary.SemAnnotVocab;
import it.uniroma2.art.uimast.Activator;
import it.uniroma2.art.uimast.impl.partial_order.Comparator;
import it.uniroma2.art.uimast.impl.partial_order.POMap;
import it.uniroma2.art.uimast.impl.partial_order.Pair;
import it.uniroma2.art.uimast.xpts.ExportModeDriver;

import java.io.File;
import java.io.FileInputStream;
import java.net.URI;
import java.util.*;

import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.iterators.FilterIterator;
import org.apache.commons.collections.iterators.ReverseListIterator;
import org.apache.uima.UIMAFramework;
import org.apache.uima.cas.*;
import org.apache.uima.resource.metadata.TypeSystemDescription;
import org.apache.uima.util.*;
import org.apache.xerces.dom.DocumentImpl;
import org.apache.xerces.parsers.DOMParser;
import org.w3c.dom.*;
import org.xml.sax.InputSource;

/**
 * Manages an ontology projection. It associates some ontology classes to an
 * annotation type in a type system.
 * 
 * @author <a href="mailto:manuel.fiorelli@gmail.com">Manuel Fiorelli</a>
 */
public class AnnotationProjection extends CommonDOMFacilities {

	/**
	 * Constructs an <code>AnnotationProjection</code> object.
	 * 
	 * @param typeSystemURI the target type system descriptor URI
	 * @param annotationBaseName annotation base type name
	 * @throws Exception
	 */
	public AnnotationProjection(URI typeSystemURI, String annotationBaseName) throws Exception {
		
		// TODO: maybe that it is better not to initialize the annotationBase
		//       until the first classProjection or feature is set.
		
		// Creates a partially ordered map to store the class projections.
		// Keys = ontology classes
		// Values = class projections
		classProjections = createPOMap();
		
		// Creates an hash set to store the annotation base type feature projections. 
		features = new HashSet<FeatureProjection>();
		
		// Stores the type system descriptor URI.
		this.typeSystemURI = typeSystemURI;
		
		//// Reads the type system (this could throw if the descriptor is invalid).
		File typeSystemFile = new File(typeSystemURI);
		XMLParser uimaParser = UIMAFramework.getXMLParser();
		TypeSystemDescription typeSystemDescription = uimaParser.parseTypeSystemDescription(new XMLInputSource(typeSystemFile));
		
		// Stores a CAS which uses the given type system. It will be used a prototype.
		cas = CasCreationUtils.createCas(typeSystemDescription, null, null);
		
		// Stores the annotation base type
		annotationBase = cas.getTypeSystem().getType(annotationBaseName);
		
		// Checks that the annotation base type exists.
		if(annotationBase == null)
			throw new Exception("Annotation base type \"" + annotationBaseName + "\" doesn't exist");
		
		// Creates the fake class projection
		fakeClassProjection = new ClassProjection(OWL.Res.THING, this, annotationBaseName);
	}

	/**
	 * Constructs an <code>AnnotationProjection</code> object from a projection descriptor.
	 * @param projectionFile a projection descriptor file
	 * @throws Exception
	 */
	public AnnotationProjection(File projectionFile) throws Exception {
		// Creates the map to store the class projections.
		classProjections = createPOMap();
		
		// Creates the hash set to store the annotation base type feature projections.
		features = new HashSet<FeatureProjection>();
		
		// Creates a DOM parser.
		DOMParser parser = new DOMParser();
		
		// Creates the input source to read the projection descriptor
		InputSource in = new InputSource(new FileInputStream(projectionFile));
		
		// Parses the projection descriptor.
		parser.parse(in);
		
		// Gets the DOM document describing the projection descriptor.
		Document doc = parser.getDocument();
		
		//// Reads the type system.
		NodeList tsList = doc.getElementsByTagNameNS(PROJ_NS,"typesystem");
		if (tsList.getLength() == 0)
			throw new Exception("Missing type system assignment");
		Element tsElement = (Element) tsList.item(0);
		if (!hasAttributeNS(tsElement, PROJ_NS, "location"))
			throw new Exception("Missing type system location");
		String typeSystemLocation = getAttributeNS(tsElement, PROJ_NS, "location");
		
		// Resolves the type system URI wrt the projection descriptor URI.
		typeSystemURI = projectionFile.toURI().resolve(typeSystemLocation);
		
		// Gets the type system descriptor.
		File typeSystemFile = new File(typeSystemURI);
		
		// Gets the UIMA XML parser.
		XMLParser uimaParser = UIMAFramework.getXMLParser();
		
		// Parses the type system descriptor.
		TypeSystemDescription typeSystemDescription = uimaParser
				.parseTypeSystemDescription(new XMLInputSource(typeSystemFile));
		
		// Instantiates a CAS with the given type system.
		cas = CasCreationUtils.createCas(typeSystemDescription, null, null);
		
		// Gets the type system (instantiated).
		TypeSystem typeSystem = cas.getTypeSystem();
		
		//// Gets the annotation base type.
		NodeList annotList = doc.getElementsByTagNameNS(PROJ_NS,"annotation");
		
		if (annotList.getLength() == 0)
			throw new Exception("Missing annotation base assignment");
		
		Element annotElement = (Element) annotList.item(0);
		
		if (!hasAttributeNS(annotElement, PROJ_NS, "type"))
			throw new Exception("Missing annotation base type");
		
		String annotationBaseTypeName = getAttributeNS(annotElement, PROJ_NS, "type");
		
		annotationBase = typeSystem.getType(annotationBaseTypeName);
		
		// Checks that the annotation base type exists.
		if (annotationBase == null)
			throw new Exception("The type \"" +	annotationBaseTypeName + "\" doesn't exist");
		
		if (!hasAttributeNS(annotElement, PROJ_NS, "mode"))
			throw new Exception("Missing mode attribute on \"" + annotElement.getNodeName() + "\"");
		
		String mode = getAttributeNS(annotElement, PROJ_NS, "mode");
		
		// Locates the export mode provider
		exportModeProvider = Activator.activator.getExportModeProvider(mode);
		
		if (exportModeProvider == null) {
			throw new Exception("Unsupported export mode \"" + mode + "\"");
		}
		
		// Creates the fake class projection.
		fakeClassProjection = new ClassProjection(OWL.Res.THING, this, annotationBaseTypeName);
		
		//// Manages the projections of the feature of the annotation base type.
		
		NodeList featureList = annotElement.getElementsByTagNameNS(PROJ_NS, "feature");
		
		for (int i = 0; i < featureList.getLength(); i++) {
			Element f = (Element) featureList.item(i);
			FeatureProjection featureProjection = FeatureProjection.create(fakeClassProjection, f);
			if (!features.add(featureProjection))
				throw new Exception("Duplicated feature definition");
		}

		//// Manages the class projection.
		
		NodeList projectionList = doc.getElementsByTagNameNS(PROJ_NS,"project");
		
		for (int i = 0; i < projectionList.getLength(); i++) {
			Element e = (Element) projectionList.item(i);
			ClassProjection classProjection = new ClassProjection(this, e);
			addClassProjection(classProjection);
		}
	}

	/**
	 * Instantiates a <code>POMap</code>.
	 * @return a map
	 */
	private POMap<ARTResource, ClassProjection> createPOMap() {
		return new POMap<ARTResource, ClassProjection>(new Comparator<ARTResource>() {

			/**
			 * Compares two ontology classes.
			 * @param l left operand
			 * @param r right operator
			 * @return <code>true</code> iff one can prove that <code>l rdfs:subClassOf r</code> 
			 */
			public boolean less_equal(ARTResource l, ARTResource r) {
				return AnnotationProjection.subclasses(l, r);
			}

		});
	}

	/**
	 * Adds a projection for a feature of the annotation base type.
	 * @param shortName the feature short name
	 * @param mode the projection mode: either <code>value</code> or <code>property</code>
	 * @param value the value lexical representation or the property URI
	 * @throws Exception
	 */
	public void addFeatureProjection(String shortName, String mode, String value) throws Exception {
		
		// Checks that annotation base type is set.
		if (annotationBase == null)
			throw new Exception("Annotation base type isn't assigned");
		
		// Creates a projection for a feature of the annotation base type.
		FeatureProjection f = FeatureProjection.create(fakeClassProjection, shortName, mode, value);
		
		// Adds the new feature projection and verify that the feature wasn't already projected.
		if (!features.add(f))
			throw new Exception("The feature \"" + f.getFeature().getName() + "\" is already projected");
	}

	/**
	 * Remove the projection for a feature of the annotation base type.
	 * @param shortName the feature short name
	 * @throws Exception
	 */
	public void removeFeatureProjection(String shortName) throws Exception {
		// Iterates over the feature projections.
		for (Iterator<FeatureProjection> it = features.iterator(); it.hasNext();)
			if (it.next().getFeature().getShortName().equals(shortName)) {	// Removes the given feature.
				it.remove();
				return;
			}

	}

	/**
	 * Gets the current CAS.
	 * @return a CAS
	 */
	public CAS getCAS() {
		return cas;
	}
	
	/**
	 * Gets the annotation base type.
	 * @return a type
	 */
	public Type getAnnotationBaseType() {
		return annotationBase;
	}

	/**
	 * Gets a class projection.
	 * @param cls an ontology class
	 * @return a class projection (otherwise null)
	 */
	public ClassProjection getClassProjection(ARTResource cls) {
		return classProjections.get(cls);
	}

	/**
	 * Adds a class projection.
	 * @param classProjection a class projection
	 * @throws Exception
	 */
	private void addClassProjection(ClassProjection classProjection) throws Exception {
		// Checks that a class isn't already projected.
		if (classProjections.containsKey(classProjection.getSourceClass()))
			throw new Exception("The class \"" + classProjection.getSourceClass().getURI() + "\" is already projected");
	
		// Gets the current CAS type system.
		TypeSystem typeSystem = cas.getTypeSystem();
		
		// Iterates over the class projections.
		for (Iterator<Pair<ARTResource, ClassProjection>> iterator = classProjections.iterator(); iterator.hasNext();) {
			// Gets the current projection.
			Pair<ARTResource, ClassProjection> pair = iterator.next();
			
			//// Checks that if two classes are in the same inheritance path,
			//// then the same relation exists between the target types.
			
			if (subclasses(classProjection.getSourceClass(),pair.first)) {
				if (!typeSystem.subsumes(pair.second.getDestinationType(), classProjection.getDestinationType())
						&& !pair.second.getDestinationType().equals(
										classProjection.getDestinationType()))
					throw new Exception("The super class \"" + pair.first.getURI() + "\" is projected into \"" + pair.second.getDestinationType().getName() + "\" which isn't equal nor subsumes the type \"" + classProjection.getDestinationType().getName()+"\" while the subclass \"" + classProjection.getSourceClass().getURI() + "\" is projected into");
			} else if (subclasses(pair.first, classProjection.getSourceClass())
					&& !typeSystem.subsumes(classProjection.getDestinationType(),
							pair.second.getDestinationType())
					&& !classProjection.getDestinationType().equals(pair.second.getDestinationType()))
				throw new Exception("The super class \"" + classProjection.getSourceClass().getURI() + "\" is projected into \"" + classProjection.getDestinationType().getName() + "\" which isn't equal nor subsumes the type \"" + pair.second.getDestinationType().getName() + "\" where the subclass \"" + pair.first.getURI() + "\" is projected into");
		}

		// Stores the new class projections.
		classProjections.add(classProjection.getSourceClass(), classProjection);
	}

	/**
	 * Add a class projection.
	 * @param className the class name
	 * @param destType the destination type fully qualified name
	 * @throws Exception
	 */
	public void addClassProjection(String className, String destType) throws Exception {
		// Gets the ontology repository interface.
		ARTRepository repository = Resources.getRepository();
		
		// Gets the class with a given name (that is an URI).
		ARTResource cls = repository.getSTClass(className);
		
		// Checks that the class exists.
		if (cls == null)
			throw new Exception("The class \"" + className + "\" doesn't exist");
		
		// Checks that the annotation base type was already defined!!!!
		if (annotationBase == null) {
			throw new Exception("The annotation base type wasn't defined yet");
		}
		
		// Adds the class projection.
		addClassProjection(new ClassProjection(cls, this,destType));	
	}

	/**
	 * Removes a class projection.
	 * @param cls an ontology class
	 * @throws Exception
	 */
	public void removeClassProjection(ARTResource cls) throws Exception {
		if (classProjections.remove(cls) == null)
			throw new Exception("The class \"" + cls.getURI() + "\" hasn't any projection");
	}

	/**
	 * Applies this projection to a web page.
	 * @param webPage a web page
	 */
	public void apply(ARTResource webPage) throws Exception{
		cas.reset();
		
		exportModeProvider.enterWebPage(webPage, cas);
		
		ARTRepository repository = Resources.getRepository();
		
		@SuppressWarnings("unchecked")
		Iterator<ARTResource> it = new FilterIterator(
				repository
						.listSTSubjectInstances(
								SemAnnotVocab.Res.location,
								webPage), rangeAnnotationPredicate);
		
		while(it.hasNext()) {
			ARTResource annot = it.next();
			Iterator<ARTResource> objIt = repository
					.listSTSubjectInstances(
							SemAnnotVocab.Res.annotation,
							annot);
			ARTResource domainObject = (ARTResource) objIt.next();
			
			apply(domainObject, annot);
		}
	}

	/**
	 * Applies the projection to an ordered pair <code>&lt;domainObject, annot&gt;</code>
	 * @param domainObject the domain object
	 * @param annot the annotation
	 */
	private void apply(ARTResource domainObject, ARTResource annot) throws Exception{
		exportModeProvider.enterAnnotation(domainObject, annot);
		
		HashMap<FeatureStructure, List<ClassProjection>> instantiatedClasses = new HashMap<FeatureStructure, List<ClassProjection>>();
		for(Pair<ARTResource, ClassProjection> pair : classProjections) {
			ARTResource sourceClass = pair.first;
			ClassProjection cp = pair.second;
			
			boolean minimal = true;
			for(FeatureStructure fs : instantiatedClasses.keySet()) {
				List<ClassProjection> cc = instantiatedClasses.get(fs);
				if (subclasses(cc.get(0).getSourceClass(), sourceClass)) {
					minimal = false;
					cc.add(cp);
				}
			}

			if (minimal && Resources.getRepository().hasType(domainObject, sourceClass)) {
				FeatureStructure fs = cas.createFS(cp.getDestinationType());
				List<ClassProjection> single = new ArrayList<ClassProjection>();
				single.add(cp);
				instantiatedClasses.put(fs, single);
			}
		}

		for (FeatureStructure fs : instantiatedClasses.keySet()) { 

			for(FeatureProjection fp : features)
				fp.apply(domainObject, annot, fs);

			List<ClassProjection> classes = instantiatedClasses.get(fs);
			
			@SuppressWarnings("unchecked")
			Iterator<ClassProjection> it = new ReverseListIterator(classes);
			
			while(it.hasNext()) {
				it.next().apply(domainObject, annot, fs);
			}
			
			cas.addFsToIndexes(fs);
		}

	}

	/**
	 * Tests if a class extends another one.
	 * @param subClass a class
	 * @param superClass a class
	 * @return <code>true</code> iff <code>subClass rdfs:subClassOf superClass</code>
	 */
	static boolean subclasses(ARTResource subClass, ARTResource superClass) {
		// Gets the ontology repository interface.
		ARTRepository repository = Resources.getRepository();
		
		// Checks that subClass extends (or is equal to) superClass
		return subClass.equals(superClass) || repository.getStatements(subClass, RDFS.Res.SUBCLASSOF, superClass).hasNext();
	}

	/** 
	 * Serializes this ontology projection.
	 * @return a DOM document
	 */
	public Document toDOM() {
		// Creates a new DOM document.
		Document doc = new DocumentImpl();
		
		// Creates the element <projection>.
		Element root = doc.createElement("projection");
		
		// Sets the document element.
		doc.appendChild(root);
		
		// Sets the default namespace.
		root.setAttribute("xmlns", PROJ_NS);
		
		// Creates the <typesystem> element.
		Element ts = doc.createElement("typesystem");
		
		// Appends it to the root.
		doc.appendChild(ts);
		
		// Stores the type system location.
		ts.setAttribute("location", typeSystemURI.toString());
		
		// Stores the annotation base type.
		Element ann = doc.createElement("annotation");
		doc.appendChild(ann);
		ann.setAttribute("type", annotationBase.getName());
		ann.setAttribute("mode", exportModeProvider.getModeName()); 
		
		// Serializes the feature projections for the annotation base type.
		for(FeatureProjection fp : features)
			fp.toDOM(ann);

		// Serializes the class projections.
		for(Pair<ARTResource, ClassProjection> pair : classProjections) {
			pair.second.toDOM(root);
		}

		// Returns the document.
		return doc;
	}

	/** {@value #PROJ_NS} */
	public static final String PROJ_NS = "http://semanticturkey.uniroma2.it/extensions/uimast/schemas/projection-20081117#";
	
	/** A predicate used to filter the individuals of the <code>ann:RangeAnnotation</code> class */
	private static Predicate rangeAnnotationPredicate = new Predicate() {

		// Evaluates this predicate against a given resource.
		public boolean evaluate(Object arg0) {
			
			// If arg0 isn't an ARTResource, then returns false.
			if (!(arg0 instanceof ARTResource))
				return false;
			
			// Casts the argument to ARTResource.
			ARTResource semanticAnnotation = (ARTResource) arg0;
			
			// Gets the ontology repository interface.
			ARTRepository repository = Resources.getRepository();
			
			// Tests if the provided resource is an instance of the class RangeAnnotation.
			return repository.hasType(semanticAnnotation, SemAnnotVocab.Res.RangeAnnotation);
		}

	};
	
    /**
     * Gets the current export mode provider.
     * @return the export mode provider
     */
    public ExportModeDriver getExportModeProvider() {
    	return exportModeProvider;
    }
    
	
	/** The export mode provider. */
	private ExportModeDriver exportModeProvider;
	
	/** The current CAS. */
	private CAS cas;
	
	/** The type system URI. */
	private URI typeSystemURI;
	
	/** The annotation base type. */
	private Type annotationBase;
	
	/** It is a fake class projection used to manage the annotation base type */
	private ClassProjection fakeClassProjection;
	
	/** The class projections. */
	private POMap<ARTResource, ClassProjection> classProjections;
	
	/** The projections of the features of the annotation base type. */
	private HashSet<FeatureProjection> features;
}
